/*-------------------------------------------------------------------------
 *
 * joinpath.c
 *	  查找处理一组连接的所有可能路径的例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/path/joinpath.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <math.h>

#include "executor/executor.h"
#include "foreign/fdwapi.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/planmain.h"
#include "optimizer/restrictinfo.h"
#include "utils/typcache.h"

/* 插件在 add_paths_to_joinrel() 中获取控制的钩子 */
set_join_pathlist_hook_type set_join_pathlist_hook = NULL;

/*
 * 父级参数化的路径可以被认为是任何子级参数化的。
 */
#define PATH_PARAM_BY_PARENT(path, rel)	\
	((path)->param_info && bms_overlap(PATH_REQ_OUTER(path),	\
									   (rel)->top_parent_relids))
#define PATH_PARAM_BY_REL_SELF(path, rel)  \
	((path)->param_info && bms_overlap(PATH_REQ_OUTER(path), (rel)->relids))

#define PATH_PARAM_BY_REL(path, rel)	\
	(PATH_PARAM_BY_REL_SELF(path, rel) || PATH_PARAM_BY_PARENT(path, rel))

static void fc_try_partial_mergejoin_path(PlannerInfo *fc_root,
									   RelOptInfo *fc_joinrel,
									   Path *fc_outer_path,
									   Path *fc_inner_path,
									   List *fc_pathkeys,
									   List *fc_mergeclauses,
									   List *fc_outersortkeys,
									   List *fc_innersortkeys,
									   JoinType fc_jointype,
									   JoinPathExtraData *fc_extra);
static void sort_inner_and_outer(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
								 RelOptInfo *fc_outerrel, RelOptInfo *fc_innerrel,
								 JoinType fc_jointype, JoinPathExtraData *fc_extra);
static inline bool fc_clause_sides_match_join(RestrictInfo *fc_rinfo,
										   RelOptInfo *fc_outerrel,
										   RelOptInfo *fc_innerrel);
static void fc_match_unsorted_outer(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
								 RelOptInfo *fc_outerrel, RelOptInfo *fc_innerrel,
								 JoinType fc_jointype, JoinPathExtraData *fc_extra);
static void fc_consider_parallel_nestloop(PlannerInfo *fc_root,
									   RelOptInfo *fc_joinrel,
									   RelOptInfo *fc_outerrel,
									   RelOptInfo *fc_innerrel,
									   JoinType fc_jointype,
									   JoinPathExtraData *fc_extra);
static void fc_consider_parallel_mergejoin(PlannerInfo *fc_root,
										RelOptInfo *fc_joinrel,
										RelOptInfo *fc_outerrel,
										RelOptInfo *fc_innerrel,
										JoinType fc_jointype,
										JoinPathExtraData *fc_extra,
										Path *fc_inner_cheapest_total);
static void fc_hash_inner_and_outer(PlannerInfo *fc_root, RelOptInfo *fc_joinrel,
								 RelOptInfo *fc_outerrel, RelOptInfo *fc_innerrel,
								 JoinType fc_jointype, JoinPathExtraData *fc_extra);
static List *fc_select_mergejoin_clauses(PlannerInfo *fc_root,
									  RelOptInfo *fc_joinrel,
									  RelOptInfo *fc_outerrel,
									  RelOptInfo *fc_innerrel,
									  List *fc_restrictlist,
									  JoinType fc_jointype,
									  bool *fc_mergejoin_allowed);
static void fc_generate_mergejoin_paths(PlannerInfo *fc_root,
									 RelOptInfo *fc_joinrel,
									 RelOptInfo *fc_innerrel,
									 Path *fc_outerpath,
									 JoinType fc_jointype,
									 JoinPathExtraData *fc_extra,
									 bool fc_useallclauses,
									 Path *fc_inner_cheapest_total,
									 List *fc_merge_pathkeys,
									 bool fc_is_partial);


/*
 * add_paths_to_joinrel
 *	  给定一个连接关系和两个组成关系，我们可以基于它们来考虑所有使用这两个组成关系作为外部和内部关系的可能路径。如果这些路径能够与其他路径进行比较并存活（并移除任何被这些路径主导的现有路径），则将这些路径添加到连接关系的路径列表中。
 *
 * 修改连接关系节点的路径列表字段，以包含迄今为止找到的最佳路径。
 *
 * jointype 不一定与 sjinfo->jointype 相同；如果我们考虑以反向方向连接关系，可能会“翻转”。
 *
 * 此例程和本模块中的其他例程接受特殊的 JoinTypes JOIN_UNIQUE_OUTER 和 JOIN_UNIQUE_INNER，以指示我们应该唯一化外部或内部关系，然后应用常规内部连接。然而，这些值不允许传播到此模块之外。路径成本估算代码可能需要识别它正在处理的情况——名义上的 jointype INNER 与 sjinfo->jointype == JOIN_SEMI 的组合就是一个指示。
 */
void add_paths_to_joinrel(PlannerInfo *fc_root,
					 RelOptInfo *fc_joinrel,
					 RelOptInfo *fc_outerrel,
					 RelOptInfo *fc_innerrel,
					 JoinType fc_jointype,
					 SpecialJoinInfo *fc_sjinfo,
					 List *fc_restrictlist)
{
	JoinPathExtraData fc_extra;
	bool		fc_mergejoin_allowed = true;
	bool		fc_consider_join_pushdown = false;
	ListCell   *fc_lc;
	Relids		fc_joinrelids;

	/*
	 * PlannerInfo 不包含子关系之间的 joins 创建的 SpecialJoinInfos，即使在最上层父级之间有一个 SpecialJoinInfo 节点。因此，在计算表示限制的 Relids 集时，考虑分区的最上层父级的 relids。
	 */
	if (fc_joinrel->reloptkind == RELOPT_OTHER_JOINREL)
		fc_joinrelids = fc_joinrel->top_parent_relids;
	else
		fc_joinrelids = fc_joinrel->relids;

	fc_extra.restrictlist = fc_restrictlist;
	fc_extra.mergeclause_list = NIL;
	fc_extra.sjinfo = fc_sjinfo;
	fc_extra.param_source_rels = NULL;

	/*
	 * 查看内部关系是否对这个外部关系可证明是唯一的。
	 *
	 * 我们有一些特殊情况：对于 JOIN_SEMI 和 JOIN_ANTI，它并不重要，因为执行者可以做出等效的优化；我们不需要在证明上浪费规划器周期。对于 JOIN_UNIQUE_INNER，我们必须考虑一个内部侧不可证明唯一的半连接（否则 reduce_unique_semijoins 会简化它），因此调用 innerrel_is_unique 是没有意义的。然而，如果 LHS 覆盖了半连接的 min_lefthand，则设置 inner_unique 是合适的，因为 create_unique_path 产生的路径相对于 LHS 是唯一的。（如果我们有一个仅部分覆盖 min_lefthand 的 LHS，那就 *不* 是正确的。）对于 JOIN_UNIQUE_OUTER，传递 JOIN_INNER 以避免让该值逃离此模块。
	 */
	switch (fc_jointype)
	{
		case JOIN_SEMI:
		case JOIN_ANTI:

			/*
			 * XXX 证明这一点可能值得，这样才能考虑在嵌套循环半连接/反连接中使用 Memoize。
			 */
			fc_extra.inner_unique = false; /* 好吧，未经证明 */
			break;
		case JOIN_UNIQUE_INNER:
			fc_extra.inner_unique = bms_is_subset(fc_sjinfo->min_lefthand,
											   fc_outerrel->relids);
			break;
		case JOIN_UNIQUE_OUTER:
			fc_extra.inner_unique = innerrel_is_unique(fc_root,
													fc_joinrel->relids,
													fc_outerrel->relids,
													fc_innerrel,
													JOIN_INNER,
													fc_restrictlist,
													false);
			break;
		default:
			fc_extra.inner_unique = innerrel_is_unique(fc_root,
													fc_joinrel->relids,
													fc_outerrel->relids,
													fc_innerrel,
													fc_jointype,
													fc_restrictlist,
													false);
			break;
	}

	/*
	 * 查找潜在的合并连接子句。如果我们不想进行合并连接，可以跳过此步骤。然而，合并连接可能是实现完全外连接的唯一方法，因此如果它是完全连接，则覆盖 enable_mergejoin。
	 */
	if (enable_mergejoin || fc_jointype == JOIN_FULL)
		fc_extra.mergeclause_list = fc_select_mergejoin_clauses(fc_root,
														  fc_joinrel,
														  fc_outerrel,
														  fc_innerrel,
														  fc_restrictlist,
														  fc_jointype,
														  &fc_mergejoin_allowed);

	/*
	 * 如果是 SEMI、ANTI 或 inner_unique 连接，计算成本估算的修正因子。这些对于所有路径都是相同的。
	 */
	if (fc_jointype == JOIN_SEMI || fc_jointype == JOIN_ANTI || fc_extra.inner_unique)
		compute_semi_anti_join_factors(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
									   fc_jointype, fc_sjinfo, fc_restrictlist,
									   &fc_extra.semifactors);

	/*
	 * 决定为这个连接关系生成参数化路径是否合理，以及如果是这样，这些路径应该需要哪些关系。通常情况下，除非存在阻止直接将我们输入的关系之一连接到参数源关系而不是连接到其他输入关系的连接顺序限制，否则没有必要创建参数化结果路径。（但请参见 allow_star_schema_join()。）这种限制减少了我们在更高连接级别中必须处理的参数化路径数量，而不影响最终计划的质量。我们将限制表示为一个 Relids 集，该集必须与任何提议的连接路径的参数化部分重叠。
	 */
	foreach(fc_lc, fc_root->join_info_list)
	{
		SpecialJoinInfo *fc_sjinfo2 = (SpecialJoinInfo *) lfirst(fc_lc);

		
/*
		 * 如果我们有其右侧的一部分 (可能不是全部)，并且尚未加入其左侧，则 SJ 与该连接相关。 
		 * （此测试相当简单，但考虑到连接已经被证明是合法的，应该足够。） 
		 * 如果 SJ 相关，它会对连接其右侧以外的任何内容施加限制。
		 */
		if (bms_overlap(fc_joinrelids, fc_sjinfo2->min_righthand) &&
			!bms_overlap(fc_joinrelids, fc_sjinfo2->min_lefthand))
			fc_extra.param_source_rels = bms_join(fc_extra.param_source_rels,
											   bms_difference(fc_root->all_baserels,
															  fc_sjinfo2->min_righthand));

		/* 完全连接对双方进行对称约束 */
		if (fc_sjinfo2->jointype == JOIN_FULL &&
			bms_overlap(fc_joinrelids, fc_sjinfo2->min_lefthand) &&
			!bms_overlap(fc_joinrelids, fc_sjinfo2->min_righthand))
			fc_extra.param_source_rels = bms_join(fc_extra.param_source_rels,
											   bms_difference(fc_root->all_baserels,
															  fc_sjinfo2->min_lefthand));
	}

	/*
	 * 然而，当涉及到 LATERAL 子查询时，连接关系将没有任何路径不是由子查询的参数化所参数化，除非其参数化 
	 * 在连接关系中被解析。因此，我们不妨允许连接关系拥有对任何剩余的横向依赖的额外依赖。
	 */
	fc_extra.param_source_rels = bms_add_members(fc_extra.param_source_rels,
											  fc_joinrel->lateral_relids);

	/*
	 * 1. 考虑必须显式排序的合并连接路径。如果我们不能合并连接，则跳过此项。
	 */
	if (fc_mergejoin_allowed)
		sort_inner_and_outer(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
							 fc_jointype, &fc_extra);

	/*
	 * 2. 考虑外部关系不需要显式排序的路径。这包括外部路径已排序的嵌套循环和合并连接。 
	 * 再次，如果我们不能合并连接，则跳过此项。（这没关系，因为我们知道嵌套循环根本无法处理右侧/完全连接，因此它在禁止的情况下也无法工作。）
	 */
	if (fc_mergejoin_allowed)
		fc_match_unsorted_outer(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
							 fc_jointype, &fc_extra);

#ifdef NOT_USED

	/*
	 * 3. 考虑内部关系不需要显式排序的路径。这仅包括合并连接（嵌套循环已经在 
	 * match_unsorted_outer 中构建过）。
	 *
	 * 由于冗余而剔除 2000 年 2 月 13 日 -- tgl。合并连接的内部和外部侧之间没有 
	 * 任何真正显著的区别，因此 match_unsorted_inner 创建的路径与 invoke add_paths_to_joinrel() 
	 * 时交换顺序给出的两个关系所制作的路径等价。
	 */
#ifndef FDD
    // 当前版本中已经没有match_unsorted_inner函数，所以注释
	if (fc_mergejoin_allowed)
		match_unsorted_inner(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
							 fc_jointype, &fc_extra);
#endif //end FDD
#endif

	/*
	 * 4. 考虑必须在连接之前先进行哈希的外部和内部关系的路径。如上所述， 
	 * 忽略完全连接的 enable_hashjoin，因为可能没有其他替代方案。
	 */
	if (enable_hashjoin || fc_jointype == JOIN_FULL)
		fc_hash_inner_and_outer(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
							 fc_jointype, &fc_extra);

	/*
	 * createplan.c 目前不支持处理由扩展推送下来的连接分配给伪常量子句； 
	 * 检查限制列表是否有此类子句，如果没有，则允许它们考虑推送下连接。
	 */
	if ((fc_joinrel->fdwroutine &&
		 fc_joinrel->fdwroutine->GetForeignJoinPaths) ||
		set_join_pathlist_hook)
		fc_consider_join_pushdown = !has_pseudoconstant_clauses(fc_root,
															 fc_restrictlist);

	/*
	 * 5. 如果内部和外部关系是属于同一服务器的外部表（或连接），并且 
	 * 分配给同一用户以检查访问权限，则给 FDW 一次推送连接的机会。
	 */
	if (fc_joinrel->fdwroutine &&
		fc_joinrel->fdwroutine->GetForeignJoinPaths &&
		fc_consider_join_pushdown)
		fc_joinrel->fdwroutine->GetForeignJoinPaths(fc_root, fc_joinrel,
												 fc_outerrel, fc_innerrel,
												 fc_jointype, &fc_extra);

	/*
	 * 6. 最后，给扩展一个机会来操作路径列表。它们可以通过调用 add_path() 
	 * 添加新路径（例如 CustomPaths），或在并行意识下调用 add_partial_path()。 
	 * 它们还可以删除或修改核心代码添加的路径。
	 */
	if (set_join_pathlist_hook &&
		fc_consider_join_pushdown)
		set_join_pathlist_hook(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
							   fc_jointype, &fc_extra);
}

/*
 * 我们重写 param_source_rels 启发式以接受外部关系满足内部路径参数 
 * 部分但不是全部参数化的嵌套循环路径。这对于获取星型模式方案的良好计划是必要的， 
 * 在该方案中，大表的参数化路径可能需要来自多个小表的参数，而这些小表不会直接连接到 
 * 彼此。我们可以通过将小表放在外部的嵌套循环来处理这个问题；但这打破了 
 * param_source_rels 启发式所基于的规则，即应在没有连接顺序约束的理由的情况下 
 * 不将参数传递到连接下方。因此，当此情况适用时，我们忽略 param_source_rels 
 * 限制。
 *
 * allow_star_schema_join() 在 param_source_rels 限制应被覆盖时返回 true，即可以执行此连接。
 */
static inline bool fc_allow_star_schema_join(PlannerInfo *fc_root,
					   Relids fc_outerrelids,
					   Relids fc_inner_paramrels)
{
	/*
	 * 如果外部关系提供了内部关系参数化的部分但不是全部，则这是一个星型模式情况。
	 */
	return (bms_overlap(fc_inner_paramrels, fc_outerrelids) &&
			bms_nonempty_difference(fc_inner_paramrels, fc_outerrelids));
}

/*
 * paraminfo_get_equal_hashops
 *		确定 param_info 和 innerrel 的 lateral_vars 是否可以被哈希。
 *		如果哈希是可能的，则返回 true，否则返回 false。
 *
 * 此外，我们还收集外部表达式和每个参数到 innerrel 的哈希操作符。 
 * 这些在我们返回 true 时设置为 'param_exprs'、'operators' 和 'binary_mode'。
 */
static bool fc_paraminfo_get_equal_hashops(PlannerInfo *fc_root, ParamPathInfo *fc_param_info,
							RelOptInfo *fc_outerrel, RelOptInfo *fc_innerrel,
							List **fc_param_exprs, List **fc_operators,
							bool *fc_binary_mode)

{
	ListCell   *fc_lc;

	*fc_param_exprs = NIL;
	*fc_operators = NIL;
	*fc_binary_mode = false;

	if (fc_param_info != NULL)
	{
		List	   *fc_clauses = fc_param_info->ppi_clauses;

		foreach(fc_lc, fc_clauses)
		{
			RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);
			OpExpr	   *fc_opexpr;
			Node	   *fc_expr;
			Oid			fc_hasheqoperator;

			fc_opexpr = (OpExpr *) fc_rinfo->clause;

			/*
			 * 如果 rinfo 不兼容则退出。我们需要一个带有 2 个参数的连接 OpExpr。
			 */
			if (!IsA(fc_opexpr, OpExpr) || list_length(fc_opexpr->args) != 2 ||
				!fc_clause_sides_match_join(fc_rinfo, fc_outerrel, fc_innerrel))
			{
				list_free(*fc_operators);
				list_free(*fc_param_exprs);
				return false;
			}

			if (fc_rinfo->outer_is_left)
			{
				fc_expr = (Node *) linitial(fc_opexpr->args);
				fc_hasheqoperator = fc_rinfo->left_hasheqoperator;
			}
			else
			{
				fc_expr = (Node *) lsecond(fc_opexpr->args);
				fc_hasheqoperator = fc_rinfo->right_hasheqoperator;
			}

			/* 如果我们无法对外部类型进行哈希，则无法进行记忆化 */
			if (!OidIsValid(fc_hasheqoperator))
			{
				list_free(*fc_operators);
				list_free(*fc_param_exprs);
				return false;
			}

			*fc_operators = lappend_oid(*fc_operators, fc_hasheqoperator);
			*fc_param_exprs = lappend(*fc_param_exprs, fc_expr);

			
/*
			 * 当连接操作符不可哈希时，可能会出现
			 * 操作符能够区分某些哈希相等操作符无法区分的情况。例如，浮点
			 * 类型的 -0.0 和 +0.0 在哈希
			 * 函数和相等函数中被认为是相等的，但是某些其他操作符
			 * 可能能够区分这两个值。这意味着我们必须将
			 * memoize 设置为二进制比较模式，以便进行逐位
			 * 比较，而不是使用哈希相等操作符时会进行的“逻辑”比较。
			 */
			if (!OidIsValid(fc_rinfo->hashjoinoperator))
				*fc_binary_mode = true;
		}
	}

	/* 现在也将任何横向变量添加到缓存键中 */
	foreach(fc_lc, fc_innerrel->lateral_vars)
	{
		Node	   *fc_expr = (Node *) lfirst(fc_lc);
		TypeCacheEntry *fc_typentry;

		/* 如果有任何不稳定的函数则拒绝 */
		if (contain_volatile_functions(fc_expr))
		{
			list_free(*fc_operators);
			list_free(*fc_param_exprs);
			return false;
		}

		fc_typentry = lookup_type_cache(exprType(fc_expr),
									 TYPECACHE_HASH_PROC | TYPECACHE_EQ_OPR);

		/* 如果没有有效的哈希等于操作符则无法使用备忘节点 */
		if (!OidIsValid(fc_typentry->hash_proc) || !OidIsValid(fc_typentry->eq_opr))
		{
			list_free(*fc_operators);
			list_free(*fc_param_exprs);
			return false;
		}

		*fc_operators = lappend_oid(*fc_operators, fc_typentry->eq_opr);
		*fc_param_exprs = lappend(*fc_param_exprs, fc_expr);

		/*
		 * 我们必须进入二进制模式，因为我们对这些侧变量的使用方式
		 * 不是很了解。请参见上面的评论，当我们为非侧变量情况设置
		 * *binary_mode时。这可能会放宽一点，如果我们有RestrictInfos
		 * 并且知道正在使用的操作符，但是对于作为函数参数的变量这类情况
		 * ，我们必须在二进制模式下操作，因为我们无法看到该函数如何使用
		 * 这些变量。
		 */
		*fc_binary_mode = true;
	}

	/* 我们可以安全地使用备忘 */
	return true;
}

/*
 * get_memoize_path
 *		如果可能的话，在'inner_path'之上创建并返回一个备忘路径。
 *		否则返回NULL。
 */
static Path * get_memoize_path(PlannerInfo *fc_root, RelOptInfo *fc_innerrel,
				 RelOptInfo *fc_outerrel, Path *fc_inner_path,
				 Path *fc_outer_path, JoinType fc_jointype,
				 JoinPathExtraData *fc_extra)
{
	RelOptInfo *fc_top_outerrel;
	List	   *fc_param_exprs;
	List	   *fc_hash_operators;
	ListCell   *fc_lc;
	bool		fc_binary_mode;

	/* 显然如果它被禁用则不可以 */
	if (!enable_memoize)
		return NULL;

	/*
	 * 除非我们期望执行多个内部扫描，否则我们可以安全地不管这些
	 * 事情。第一次扫描总是缓存失效。根据成本，这可能会在之后
	 * 失败，因此这实际上只是为了节省一些无谓的努力。
	 */
	if (fc_outer_path->parent->rows < 2)
		return NULL;

	/*
	 * 只有在某种缓存键存在时，我们才能有备忘节点，可能是参数化
	 * 的路径子句或侧变量。没有缓存键听起来更像是一个Materialize
	 * 节点可能会更有用。
	 */
	if ((fc_inner_path->param_info == NULL ||
		 fc_inner_path->param_info->ppi_clauses == NIL) &&
		fc_innerrel->lateral_vars == NIL)
		return NULL;

	/*
	 * 目前我们不对SEMI和ANTI连接执行此操作，除非它们被标记为
	 * inner_unique。这是因为嵌套循环的SEMI/ANTI连接不会完整扫描
	 * 内部节点，这将意味着备忘无法标记缓存条目为完成。
	 *
	 * XXX 当前我们不尝试将SEMI/ANTI连接标记为inner_unique=true。
	 * 我们应该这样做吗？请参见add_paths_to_joinrel()
	 */
	if (!fc_extra->inner_unique && (fc_jointype == JOIN_SEMI ||
								 fc_jointype == JOIN_ANTI))
		return NULL;

	/*
	 * 备忘通常会在耗尽要从其子计划读取的元组时将缓存条目标记为完成。
	 * 然而，对于唯一连接，嵌套循环将在找到第一个匹配的内部元组后
	 * 跳过到下一个外部元组。这意味着我们可能不会读取连接的内部部分
	 * 直到完成，这没有机会将缓存条目标记为完成。为了解决这个问题，
	 * 当连接是唯一的时，我们会在获取第一个元组后自动标记缓存条目为完成。
	 * 当整个连接条件是参数化时，这有效。否则，当参数化只是连接条件的一个
	 * 子集时，我们无法确定它的哪部分导致连接是唯一的。这意味着没有
	 * 保证只会读取1个元组。在没有这种保证的情况下，我们无法在读取第一个
	 * 元组后将缓存条目标记为完成。这意味着备忘节点的有效范围仅限于
	 * 没有连接伙伴的外部行，因为这是嵌套循环耗尽唯一连接的内部扫描的
	 * 唯一情况。由于范围有限，我们不妨在这种情况下不创建备忘路径。
	 *
	 * 侧变量在此不必考虑，因为在确定连接是否唯一时不会考虑它们。
	 *
	 * XXX 如果将其余连接条件作为内部扫描的过滤器而不是连接过滤器来处理，
	 * 这可能会被启用。也许值得考虑这样做？
	 */
	if (fc_extra->inner_unique &&
		(fc_inner_path->param_info == NULL ||
		 list_length(fc_inner_path->param_info->ppi_clauses) <
		 list_length(fc_extra->restrictlist)))
		return NULL;

	/*
	 * 如果内部关系的目标列表或限制列表中存在不稳定函数，则无法
	 * 使用备忘节点。缓存命中可能会减少对这些函数的调用次数。
	 */
	if (contain_volatile_functions((Node *) fc_innerrel->reltarget))
		return NULL;

	foreach(fc_lc, fc_innerrel->baserestrictinfo)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		if (contain_volatile_functions((Node *) fc_rinfo))
			return NULL;
	}

	/*
	 * 还要检查参数化路径限制信息中的不稳定函数。索引函数必须是
	 * 不可变的，因此不应该有任何不稳定函数，但是，对于侧连接，内部
	 * 扫描可能不是索引扫描。
	 */
	if (fc_inner_path->param_info != NULL)
	{
		foreach(fc_lc, fc_inner_path->param_info->ppi_clauses)
		{
			RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

			if (contain_volatile_functions((Node *) fc_rinfo))
				return NULL;
		}
	}

	/*
	 * 在考虑分区连接时，我们有一些子句引用了outerrel的顶级父而非
	 * outerrel本身。
	 */
	if (fc_outerrel->reloptkind == RELOPT_OTHER_MEMBER_REL)
		fc_top_outerrel = find_base_rel(fc_root, bms_singleton_member(fc_outerrel->top_parent_relids));
	else if (fc_outerrel->reloptkind == RELOPT_OTHER_JOINREL)
		fc_top_outerrel = find_join_rel(fc_root, fc_outerrel->top_parent_relids);
	else
		fc_top_outerrel = fc_outerrel;

	/* 检查每个路径参数是否有哈希操作 */
	if (fc_paraminfo_get_equal_hashops(fc_root,
									fc_inner_path->param_info,
									fc_top_outerrel,
									fc_innerrel,
									&fc_param_exprs,
									&fc_hash_operators,
									&fc_binary_mode))
	{
		return (Path *) create_memoize_path(fc_root,
											fc_innerrel,
											fc_inner_path,
											fc_param_exprs,
											fc_hash_operators,
											fc_extra->inner_unique,
											fc_binary_mode,
											fc_outer_path->rows);
	}

	return NULL;
}

/*
 * try_nestloop_path
 *	  考虑一个嵌套循环连接路径；如果它看起来有用，则通过add_path()
 *	  将其推入连接关系的路径列表。
 */
static void fc_try_nestloop_path(PlannerInfo *fc_root,
				  RelOptInfo *fc_joinrel,
				  Path *fc_outer_path,
				  Path *fc_inner_path,
				  List *fc_pathkeys,
				  JoinType fc_jointype,
				  JoinPathExtraData *fc_extra)
{
	Relids		fc_required_outer;
	JoinCostWorkspace fc_workspace;
	RelOptInfo *fc_innerrel = fc_inner_path->parent;
	RelOptInfo *fc_outerrel = fc_outer_path->parent;
	Relids		fc_innerrelids;
	Relids		fc_outerrelids;
	Relids		fc_inner_paramrels = PATH_REQ_OUTER(fc_inner_path);
	Relids		fc_outer_paramrels = PATH_REQ_OUTER(fc_outer_path);

	/*
	 * 路径由顶级父级参数化，因此在父级relids上运行参数化测试。
	 */
	if (fc_innerrel->top_parent_relids)
		fc_innerrelids = fc_innerrel->top_parent_relids;
	else
		fc_innerrelids = fc_innerrel->relids;

	if (fc_outerrel->top_parent_relids)
		fc_outerrelids = fc_outerrel->top_parent_relids;
	else
		fc_outerrelids = fc_outerrel->relids;

	/*
	 * 检查建议的路径是否仍然是参数化的，如果参数化不合理则拒绝——
	 * 除非allow_star_schema_join表示无论如何都允许它。此外，如果
	 * have_dangerous_phv对此表示不喜欢，也必须拒绝，这只会发生在
	 * 嵌套循环仍然是参数化的情况下。
	 */
	fc_required_outer = calc_nestloop_required_outer(fc_outerrelids, fc_outer_paramrels,
												  fc_innerrelids, fc_inner_paramrels);
	if (fc_required_outer &&
		((!bms_overlap(fc_required_outer, fc_extra->param_source_rels) &&
		  !fc_allow_star_schema_join(fc_root, fc_outerrelids, fc_inner_paramrels)) ||
		 have_dangerous_phv(fc_root, fc_outerrelids, fc_inner_paramrels)))
	{
		/* 在此拒绝路径时不浪费内存 */
		bms_free(fc_required_outer);
		return;
	}

	
/*
	 * 做一个预检查以快速消除明显劣质路径。我们
	 * 计算路径成本的一个廉价下界，然后使用
	 * add_path_precheck() 来查看该路径是否明显会被
	 * 连接关系的某个现有路径所主导。如果没有，进行完整的推送，
	 * 创建一个完全有效的路径结构并提交给 add_path()。
	 * 后两个步骤的开销足够大，使这种两阶段的方法论值得。
	 */
	initial_cost_nestloop(fc_root, &fc_workspace, fc_jointype,
						  fc_outer_path, fc_inner_path, fc_extra);

	if (add_path_precheck(fc_joinrel,
						  fc_workspace.startup_cost, fc_workspace.total_cost,
						  fc_pathkeys, fc_required_outer))
	{
		/*
		 * 如果内部路径是参数化的，它是由外部关系的
		 * 最上层父关系参数化的，而不是外部关系本身。修正
		 * 这个问题。
		 */
		if (PATH_PARAM_BY_PARENT(fc_inner_path, fc_outer_path->parent))
		{
			fc_inner_path = reparameterize_path_by_child(fc_root, fc_inner_path,
													  fc_outer_path->parent);

			/*
			 * 如果我们无法转换该路径，就无法创建嵌套循环
			 * 路径。
			 */
			if (!fc_inner_path)
			{
				bms_free(fc_required_outer);
				return;
			}
		}

		add_path(fc_joinrel, (Path *)
				 create_nestloop_path(fc_root,
									  fc_joinrel,
									  fc_jointype,
									  &fc_workspace,
									  fc_extra,
									  fc_outer_path,
									  fc_inner_path,
									  fc_extra->restrictlist,
									  fc_pathkeys,
									  fc_required_outer));
	}
	else
	{
		/* 在此拒绝路径时不浪费内存 */
		bms_free(fc_required_outer);
	}
}

/*
 * try_partial_nestloop_path
 *	  考虑一个部分嵌套循环连接路径；如果它看起来有用，
 *	  通过 add_partial_path() 将其推入连接关系的 partial_pathlist。
 */
static void fc_try_partial_nestloop_path(PlannerInfo *fc_root,
						  RelOptInfo *fc_joinrel,
						  Path *fc_outer_path,
						  Path *fc_inner_path,
						  List *fc_pathkeys,
						  JoinType fc_jointype,
						  JoinPathExtraData *fc_extra)
{
	JoinCostWorkspace fc_workspace;

	/*
	 * 如果内部路径是参数化的，则该参数化必须由提议的外部路径完全
	 * 满足。参数化的部分路径不受支持。调用者应该已经验证
	 * 此处不需要任何侧向关系。
	 */
	Assert(bms_is_empty(fc_joinrel->lateral_relids));
	if (fc_inner_path->param_info != NULL)
	{
		Relids		fc_inner_paramrels = fc_inner_path->param_info->ppi_req_outer;
		RelOptInfo *fc_outerrel = fc_outer_path->parent;
		Relids		fc_outerrelids;

		/*
		 * 内部和外部路径的参数化（如果有）是由顶层
		 * 父关系而不是子关系来完成的，因此我们必须使用这些关系ID
		 * 来进行参数化测试。
		 */
		if (fc_outerrel->top_parent_relids)
			fc_outerrelids = fc_outerrel->top_parent_relids;
		else
			fc_outerrelids = fc_outerrel->relids;

		if (!bms_is_subset(fc_inner_paramrels, fc_outerrelids))
			return;
	}

	/*
	 * 在创建路径之前，快速了解它可能的成本的下界。
	 * 如果看起来糟糕，就立即退出。
	 */
	initial_cost_nestloop(fc_root, &fc_workspace, fc_jointype,
						  fc_outer_path, fc_inner_path, fc_extra);
	if (!add_partial_path_precheck(fc_joinrel, fc_workspace.total_cost, fc_pathkeys))
		return;

	/*
	 * 如果内部路径是参数化的，它是由外部关系的最上层
	 * 父关系参数化的，而不是外部关系本身。修正这个问题。
	 */
	if (PATH_PARAM_BY_PARENT(fc_inner_path, fc_outer_path->parent))
	{
		fc_inner_path = reparameterize_path_by_child(fc_root, fc_inner_path,
												  fc_outer_path->parent);

		/*
		 * 如果我们无法翻译路径，就无法创建嵌套循环路径。
		 */
		if (!fc_inner_path)
			return;
	}

	/* 可能足够好，值得尝试，所以我们尝试一下。 */
	add_partial_path(fc_joinrel, (Path *)
					 create_nestloop_path(fc_root,
										  fc_joinrel,
										  fc_jointype,
										  &fc_workspace,
										  fc_extra,
										  fc_outer_path,
										  fc_inner_path,
										  fc_extra->restrictlist,
										  fc_pathkeys,
										  NULL));
}

/*
 * try_mergejoin_path
 *	  考虑一个合并连接路径；如果它看起来有用，就通过 add_path() 将其放入
 *	  joinrel 的 pathlist 中。
 */
static void fc_try_mergejoin_path(PlannerInfo *fc_root,
				   RelOptInfo *fc_joinrel,
				   Path *fc_outer_path,
				   Path *fc_inner_path,
				   List *fc_pathkeys,
				   List *fc_mergeclauses,
				   List *fc_outersortkeys,
				   List *fc_innersortkeys,
				   JoinType fc_jointype,
				   JoinPathExtraData *fc_extra,
				   bool fc_is_partial)
{
	Relids		fc_required_outer;
	JoinCostWorkspace fc_workspace;

	if (fc_is_partial)
	{
		fc_try_partial_mergejoin_path(fc_root,
								   fc_joinrel,
								   fc_outer_path,
								   fc_inner_path,
								   fc_pathkeys,
								   fc_mergeclauses,
								   fc_outersortkeys,
								   fc_innersortkeys,
								   fc_jointype,
								   fc_extra);
		return;
	}

	/*
	 * 检查提议的路径是否仍然是参数化的，如果参数化不合理，则拒绝。
	 */
	fc_required_outer = calc_non_nestloop_required_outer(fc_outer_path,
													  fc_inner_path);
	if (fc_required_outer &&
		!bms_overlap(fc_required_outer, fc_extra->param_source_rels))
	{
		/* 在此拒绝路径时不浪费内存 */
		bms_free(fc_required_outer);
		return;
	}

	/*
	 * 如果给定的路径已经有足够的顺序，我们可以跳过显式排序。
	 */
	if (fc_outersortkeys &&
		pathkeys_contained_in(fc_outersortkeys, fc_outer_path->pathkeys))
		fc_outersortkeys = NIL;
	if (fc_innersortkeys &&
		pathkeys_contained_in(fc_innersortkeys, fc_inner_path->pathkeys))
		fc_innersortkeys = NIL;

	/*
	 * 请查看 try_nestloop_path() 中的注释。
	 */
	initial_cost_mergejoin(fc_root, &fc_workspace, fc_jointype, fc_mergeclauses,
						   fc_outer_path, fc_inner_path,
						   fc_outersortkeys, fc_innersortkeys,
						   fc_extra);

	if (add_path_precheck(fc_joinrel,
						  fc_workspace.startup_cost, fc_workspace.total_cost,
						  fc_pathkeys, fc_required_outer))
	{
		add_path(fc_joinrel, (Path *)
				 create_mergejoin_path(fc_root,
									   fc_joinrel,
									   fc_jointype,
									   &fc_workspace,
									   fc_extra,
									   fc_outer_path,
									   fc_inner_path,
									   fc_extra->restrictlist,
									   fc_pathkeys,
									   fc_required_outer,
									   fc_mergeclauses,
									   fc_outersortkeys,
									   fc_innersortkeys));
	}
	else
	{
		/* 在此拒绝路径时不浪费内存 */
		bms_free(fc_required_outer);
	}
}

/*
 * try_partial_mergejoin_path
 *	  考虑一个部分合并连接路径；如果它看起来有用，就通过 add_partial_path() 将其放入
 *	  joinrel 的 pathlist 中。
 */
static void fc_try_partial_mergejoin_path(PlannerInfo *fc_root,
						   RelOptInfo *fc_joinrel,
						   Path *fc_outer_path,
						   Path *fc_inner_path,
						   List *fc_pathkeys,
						   List *fc_mergeclauses,
						   List *fc_outersortkeys,
						   List *fc_innersortkeys,
						   JoinType fc_jointype,
						   JoinPathExtraData *fc_extra)
{
	JoinCostWorkspace fc_workspace;

	/*
	 * 请查看 try_partial_hashjoin_path() 中的注释。
	 */
	Assert(bms_is_empty(fc_joinrel->lateral_relids));
	if (fc_inner_path->param_info != NULL)
	{
		Relids		fc_inner_paramrels = fc_inner_path->param_info->ppi_req_outer;

		if (!bms_is_empty(fc_inner_paramrels))
			return;
	}

	/*
	 * 如果给定的路径已经有足够的顺序，我们可以跳过显式排序。
	 */
	if (fc_outersortkeys &&
		pathkeys_contained_in(fc_outersortkeys, fc_outer_path->pathkeys))
		fc_outersortkeys = NIL;
	if (fc_innersortkeys &&
		pathkeys_contained_in(fc_innersortkeys, fc_inner_path->pathkeys))
		fc_innersortkeys = NIL;

	/*
	 * 请查看 try_partial_nestloop_path() 中的注释。
	 */
	initial_cost_mergejoin(fc_root, &fc_workspace, fc_jointype, fc_mergeclauses,
						   fc_outer_path, fc_inner_path,
						   fc_outersortkeys, fc_innersortkeys,
						   fc_extra);

	if (!add_partial_path_precheck(fc_joinrel, fc_workspace.total_cost, fc_pathkeys))
		return;

	/* 可能足够好，值得尝试，所以我们尝试一下。 */
	add_partial_path(fc_joinrel, (Path *)
					 create_mergejoin_path(fc_root,
										   fc_joinrel,
										   fc_jointype,
										   &fc_workspace,
										   fc_extra,
										   fc_outer_path,
										   fc_inner_path,
										   fc_extra->restrictlist,
										   fc_pathkeys,
										   NULL,
										   fc_mergeclauses,
										   fc_outersortkeys,
										   fc_innersortkeys));
}

/*
 * try_hashjoin_path
 *	  考虑一个哈希连接路径；如果它看起来有用，就通过 add_path() 将其放入
 *	  joinrel 的 pathlist 中。
 */
static void fc_try_hashjoin_path(PlannerInfo *fc_root,
				  RelOptInfo *fc_joinrel,
				  Path *fc_outer_path,
				  Path *fc_inner_path,
				  List *fc_hashclauses,
				  JoinType fc_jointype,
				  JoinPathExtraData *fc_extra)
{
	Relids		fc_required_outer;
	JoinCostWorkspace fc_workspace;

	/*
	 * 检查提议的路径是否仍然是参数化的，如果参数化不合理，则拒绝。
	 */
	fc_required_outer = calc_non_nestloop_required_outer(fc_outer_path,
													  fc_inner_path);
	if (fc_required_outer &&
		!bms_overlap(fc_required_outer, fc_extra->param_source_rels))
	{
		/* 在此拒绝路径时不浪费内存 */
		bms_free(fc_required_outer);
		return;
	}

	/*
	 * 请查看 try_nestloop_path() 中的注释。 还要注意，哈希连接路径
	 * 永远不会有任何输出路径键，具体请参见 create_hashjoin_path 中的注释。
	 */
	initial_cost_hashjoin(fc_root, &fc_workspace, fc_jointype, fc_hashclauses,
						  fc_outer_path, fc_inner_path, fc_extra, false);

	if (add_path_precheck(fc_joinrel,
						  fc_workspace.startup_cost, fc_workspace.total_cost,
						  NIL, fc_required_outer))
	{
		add_path(fc_joinrel, (Path *)
				 create_hashjoin_path(fc_root,
									  fc_joinrel,
									  fc_jointype,
									  &fc_workspace,
									  fc_extra,
									  fc_outer_path,
									  fc_inner_path,
									  false,	/* parallel_hash */
									  fc_extra->restrictlist,
									  fc_required_outer,
									  fc_hashclauses));
	}
	else
	{
		/* 在此拒绝路径时不浪费内存 */
		bms_free(fc_required_outer);
	}
}

/*
 * try_partial_hashjoin_path
 *	  考虑一个部分哈希连接路径；如果它看起来有用，就通过 add_partial_path() 将其放入
 *	  joinrel 的 partial_pathlist 中。
 *	  外侧是部分的。如果 parallel_hash 为真，那么内部路径
 *	  必须是部分的，并将并行运行以创建一个或多个共享
 *	  哈希表；否则，内部路径必须是完整的并且在每个进程中
 *	  运行其副本以创建单独的相同私有哈希表。
 */
static void fc_try_partial_hashjoin_path(PlannerInfo *fc_root,
						  RelOptInfo *fc_joinrel,
						  Path *fc_outer_path,
						  Path *fc_inner_path,
						  List *fc_hashclauses,
						  JoinType fc_jointype,
						  JoinPathExtraData *fc_extra,
						  bool fc_parallel_hash)
{
	JoinCostWorkspace fc_workspace;

	/*
	 * 如果内部路径是参数化的，则该参数化必须由提议的外部路径完全
	 * 满足。参数化的部分路径不受支持。调用者应该已经验证
	 * 此处不需要任何侧向关系。
	 */
	Assert(bms_is_empty(fc_joinrel->lateral_relids));
	if (fc_inner_path->param_info != NULL)
	{
		Relids		fc_inner_paramrels = fc_inner_path->param_info->ppi_req_outer;

		if (!bms_is_empty(fc_inner_paramrels))
			return;
	}

	/*
	 * 在创建路径之前，快速了解它可能的成本的下界。
	 * 如果看起来糟糕，就立即退出。
	 */
	initial_cost_hashjoin(fc_root, &fc_workspace, fc_jointype, fc_hashclauses,
						  fc_outer_path, fc_inner_path, fc_extra, fc_parallel_hash);
	if (!add_partial_path_precheck(fc_joinrel, fc_workspace.total_cost, NIL))
		return;

	/* 可能足够好，值得尝试，所以我们尝试一下。 */
	add_partial_path(fc_joinrel, (Path *)
					 create_hashjoin_path(fc_root,
										  fc_joinrel,
										  fc_jointype,
										  &fc_workspace,
										  fc_extra,
										  fc_outer_path,
										  fc_inner_path,
										  fc_parallel_hash,
										  fc_extra->restrictlist,
										  NULL,
										  fc_hashclauses));
}

/*
 * clause_sides_match_join
 *	  确定一个连接子句是否适合在此连接中使用。
 *
 * 我们已经知道该子句是一个只引用当前连接中关系的二元操作子句。
 * 这里的关键是检查它是否具有“outerrel_expr op innerrel_expr”或
 * “innerrel_expr op outerrel_expr”的形式，而不是在任一侧混合外部和内部变量。
 * 如果匹配，我们设置临时标志 outer_is_left 来识别哪个侧是哪个。
 */
static inline bool fc_clause_sides_match_join(RestrictInfo *fc_rinfo, RelOptInfo *fc_outerrel,
						RelOptInfo *fc_innerrel)
{
	if (bms_is_subset(fc_rinfo->left_relids, fc_outerrel->relids) &&
		bms_is_subset(fc_rinfo->right_relids, fc_innerrel->relids))
	{
		/* 左侧是外部 */
		fc_rinfo->outer_is_left = true;
		return true;
	}
	else if (bms_is_subset(fc_rinfo->left_relids, fc_innerrel->relids) &&
			 bms_is_subset(fc_rinfo->right_relids, fc_outerrel->relids))
	{
		/* 右侧是外部 */
		fc_rinfo->outer_is_left = false;
		return true;
	}
	return false;				/* 对这些输入关系不适用 */
}

/*
 * sort_inner_and_outer
 *	  通过显式排序外部和内部连接关系的每个可用合并顺序来创建合并连接路径。
 *
 * 'joinrel' 是连接关系
 * 'outerrel' 是外部连接关系
 * 'innerrel' 是内部连接关系
 * 'jointype' 是要执行的连接类型
 * 'extra' 包含额外的输入值
 */
static void sort_inner_and_outer(PlannerInfo *fc_root,
					 RelOptInfo *fc_joinrel,
					 RelOptInfo *fc_outerrel,
					 RelOptInfo *fc_innerrel,
					 JoinType fc_jointype,
					 JoinPathExtraData *fc_extra)
{
	JoinType	fc_save_jointype = fc_jointype;
	Path	   *fc_outer_path;
	Path	   *fc_inner_path;
	Path	   *fc_cheapest_partial_outer = NULL;
	Path	   *fc_cheapest_safe_inner = NULL;
	List	   *fc_all_pathkeys;
	ListCell   *l;

	/*
	 * 我们只考虑最便宜的总成本输入路径，因为我们假设这里需要排序。
	 * 我们稍后会考虑最便宜的启动成本输入路径，且仅在它们不需要排序时。
	 *
	 * 此函数故意不考虑参数化输入路径，除非最便宜的总成本是参数化的。
	 * 如果我们这样做，就会产生可疑价值的合并连接路径的组合爆炸。
	 * 这与其他地方的决策相互作用，这些决策也对参数化输入的合并连接进行歧视；请参见
	 * src/backend/optimizer/README 中的注释。
	 */
	fc_outer_path = fc_outerrel->cheapest_total_path;
	fc_inner_path = fc_innerrel->cheapest_total_path;

	/*
	 * 如果任一最便宜的总路径是由其他关系参数化的，我们
	 * 就无法使用合并连接。 （寻找其他输入路径没有意义，因为这些路径应该已经是
	 * 最少参数化的可用路径。）
	 */
	if (PATH_PARAM_BY_REL(fc_outer_path, fc_innerrel) ||
		PATH_PARAM_BY_REL(fc_inner_path, fc_outerrel))
		return;

	/*
	 * 如果请求唯一化，就执行并处理为普通
	 * 内连接。
	 */
	if (fc_jointype == JOIN_UNIQUE_OUTER)
	{
		fc_outer_path = (Path *) create_unique_path(fc_root, fc_outerrel,
												 fc_outer_path, fc_extra->sjinfo);
		Assert(fc_outer_path);
		fc_jointype = JOIN_INNER;
	}
	else if (fc_jointype == JOIN_UNIQUE_INNER)
	{
		fc_inner_path = (Path *) create_unique_path(fc_root, fc_innerrel,
												 fc_inner_path, fc_extra->sjinfo);
		Assert(fc_inner_path);
		fc_jointype = JOIN_INNER;
	}

	/*
	 * 如果连接关系是并行安全的，我们可能能够考虑一个部分
	 * 合并连接。然而，我们无法处理 JOIN_UNIQUE_OUTER，因为外部路径将是部分的，
	 * 因此我们无法正确保证唯一性。同样，我们无法处理 JOIN_FULL 和
	 * JOIN_RIGHT，因为它们可能产生错误的空值扩展行。同时，
	 * 结果路径必须是不参数化的。
	 */
	if (fc_joinrel->consider_parallel &&
		fc_save_jointype != JOIN_UNIQUE_OUTER &&
		fc_save_jointype != JOIN_FULL &&
		fc_save_jointype != JOIN_RIGHT &&
		fc_outerrel->partial_pathlist != NIL &&
		bms_is_empty(fc_joinrel->lateral_relids))
	{
		fc_cheapest_partial_outer = (Path *) linitial(fc_outerrel->partial_pathlist);

		if (fc_inner_path->parallel_safe)
			fc_cheapest_safe_inner = fc_inner_path;
		else if (fc_save_jointype != JOIN_UNIQUE_INNER)
			fc_cheapest_safe_inner =
				get_cheapest_parallel_safe_total_inner(fc_innerrel->pathlist);
	}

	
/*
	 * 每种可用的合并连接子句的排列将以基本相同的成本生成
	 * 不同排序的结果路径。在这种连接层面上，我们没有理由
	 * 选择某一个，但某些排序可能比其他排序对更高层次的
	 * 合并连接更有用，因此值得考虑多种排序。
	 *
	 * 实际上，并不是每种合并子句的排列都会生成不同的路径顺序，
	 * 因为某些子句可能是部分冗余的（引用相同的等价类）。
	 * 因此，我们所做的是将合并子句列表转换为标准路径键
	 * 的列表，然后考虑路径键的不同排序。
	 *
	 * 为*每个*路径键的排列生成路径似乎不是一个成功的策略；
	 * 规划时间的成本太高。现在，我们为每个路径键生成一个路径，
	 * 将该路径键列在前面，其余的随机排序。这应该至少允许一个
	 * 子句的合并连接，而不需要重新排序与任何其他可能的合并
	 * 连接路径。但是，如果我们没有猜到次要键的正确排序，
	 * 我们可能最终将子句作为 qpquals 进行评估，而本可以
	 * 作为合并子句处理。（在实践中，合并子句超过两个或三个的
	 * 情况非常少，因此在这方面花费大量时间可能不值得。）
	 *
	 * select_outer_pathkeys_for_merge() 返回的路径键顺序背后有
	 * 一些启发式逻辑（见该函数），因此务必尽量原样尝试以及
	 * 制作变体。
	 */
	fc_all_pathkeys = select_outer_pathkeys_for_merge(fc_root,
												   fc_extra->mergeclause_list,
												   fc_joinrel);

	foreach(l, fc_all_pathkeys)
	{
		PathKey    *fc_front_pathkey = (PathKey *) lfirst(l);
		List	   *fc_cur_mergeclauses;
		List	   *fc_outerkeys;
		List	   *fc_innerkeys;
		List	   *fc_merge_pathkeys;

		/* 先把这个用户放在路径键列表中 */
		if (l != list_head(fc_all_pathkeys))
			fc_outerkeys = lcons(fc_front_pathkey,
							  list_delete_nth_cell(list_copy(fc_all_pathkeys),
												   foreach_current_index(l)));
		else
			fc_outerkeys = fc_all_pathkeys;	/* 第一个没有工作... */

		/* 将合并子句排序为相应的顺序 */
		fc_cur_mergeclauses =
			find_mergeclauses_for_outer_pathkeys(fc_root,
												 fc_outerkeys,
												 fc_extra->mergeclause_list);

		/* 应该都使用过... */
		Assert(list_length(fc_cur_mergeclauses) == list_length(fc_extra->mergeclause_list));

		/* 为内侧构建排序路径键 */
		fc_innerkeys = make_inner_pathkeys_for_merge(fc_root,
												  fc_cur_mergeclauses,
												  fc_outerkeys);

		/* 构建表示输出排序顺序的路径键 */
		fc_merge_pathkeys = build_join_pathkeys(fc_root, fc_joinrel, fc_jointype,
											 fc_outerkeys);

		/*
		 * 现在我们可以构建路径。
		 *
		 * 注：最便宜的路径可能已经正确排序。try_mergejoin_path 将检测到这种情况并抑制
		 * 显式排序步骤，因此我们在这里不需要进行排序。
		 */
		fc_try_mergejoin_path(fc_root,
						   fc_joinrel,
						   fc_outer_path,
						   fc_inner_path,
						   fc_merge_pathkeys,
						   fc_cur_mergeclauses,
						   fc_outerkeys,
						   fc_innerkeys,
						   fc_jointype,
						   fc_extra,
						   false);

		/*
		 * 如果我们有部分外部和并行安全的内部路径，那么尝试
		 * 部分合并连接路径。
		 */
		if (fc_cheapest_partial_outer && fc_cheapest_safe_inner)
			fc_try_partial_mergejoin_path(fc_root,
									   fc_joinrel,
									   fc_cheapest_partial_outer,
									   fc_cheapest_safe_inner,
									   fc_merge_pathkeys,
									   fc_cur_mergeclauses,
									   fc_outerkeys,
									   fc_innerkeys,
									   fc_jointype,
									   fc_extra);
	}
}

/*
 * generate_mergejoin_paths
 * 为输入外部路径创建可能的合并连接路径。
 *
 * 如果可用合并连接子句，我们将生成合并连接。我们有
 * 两种方法来生成合并连接的内部路径：对最便宜的
 * 内部路径进行排序，或者使用已经适合进行
 * 合并的内部路径。如果我们有多个合并子句，可能
 * 会没有内部路径（或只有一个非常昂贵的路径）来满足
 * 所有合并子句，但如果我们截断合并子句列表（从而丢弃
 * 一些排序键要求），则可能会存在更好的路径。因此，我们考虑合并子句列表的截断
 * 以及完整列表。 （理想情况下，我们会考虑合并子句列表的所有
 * 子集，但这似乎成本过高。）
 */
static void fc_generate_mergejoin_paths(PlannerInfo *fc_root,
						 RelOptInfo *fc_joinrel,
						 RelOptInfo *fc_innerrel,
						 Path *fc_outerpath,
						 JoinType fc_jointype,
						 JoinPathExtraData *fc_extra,
						 bool fc_useallclauses,
						 Path *fc_inner_cheapest_total,
						 List *fc_merge_pathkeys,
						 bool fc_is_partial)
{
	List	   *fc_mergeclauses;
	List	   *fc_innersortkeys;
	List	   *fc_trialsortkeys;
	Path	   *fc_cheapest_startup_inner;
	Path	   *fc_cheapest_total_inner;
	JoinType	fc_save_jointype = fc_jointype;
	int			fc_num_sortkeys;
	int			fc_sortkeycnt;

	if (fc_jointype == JOIN_UNIQUE_OUTER || fc_jointype == JOIN_UNIQUE_INNER)
		fc_jointype = JOIN_INNER;

	/* 查找有用的合并子句（如果有的话） */
	fc_mergeclauses =
		find_mergeclauses_for_outer_pathkeys(fc_root,
											 fc_outerpath->pathkeys,
											 fc_extra->mergeclause_list);

	/*
	 * 如果没有合并连接的机会则完成此外部路径。
	 *
	 * 特殊角落情况：对于 "x FULL JOIN y ON true"，将
	 * 根本没有连接子句。通常我们会生成一个没有子句的嵌套循环路径，
	 * 但由于合并连接是我们唯一支持 FULL JOIN
	 * 而没有任何连接子句的连接类型，因此有必要生成一个没有子句的
	 * 合并连接路径。
	 */
	if (fc_mergeclauses == NIL)
	{
		if (fc_jointype == JOIN_FULL)
			 /* 可以尝试合并连接 */ ;
		else
			return;
	}
	if (fc_useallclauses &&
		list_length(fc_mergeclauses) != list_length(fc_extra->mergeclause_list))
		return;

	/* 计算内部路径所需的排序 */
	fc_innersortkeys = make_inner_pathkeys_for_merge(fc_root,
												  fc_mergeclauses,
												  fc_outerpath->pathkeys);

	/*
	 * 根据对最便宜的内部路径进行排序生成合并连接。由于
	 * 需要进行排序，因此只有最便宜的总成本才重要。（但是
	 * try_mergejoin_path 如果 inner_cheapest_total 已经
	 * 正确排序则会执行正确的操作。）
	 */
	fc_try_mergejoin_path(fc_root,
					   fc_joinrel,
					   fc_outerpath,
					   fc_inner_cheapest_total,
					   fc_merge_pathkeys,
					   fc_mergeclauses,
					   NIL,
					   fc_innersortkeys,
					   fc_jointype,
					   fc_extra,
					   fc_is_partial);

	/* 如果内部路径需要唯一化，则无法执行其他操作 */
	if (fc_save_jointype == JOIN_UNIQUE_INNER)
		return;

	/*
	 * 查找满足 innersortkey 列表的预排序内部路径 ---
	 * 如果我们被允许使用合并子句的子集构建合并连接，则
	 * 可以考虑其任何截断。这里，我们考虑低启动成本
	 * 和低总成本。
	 *
	 * 目前我们在此不考虑参数化内部路径。这与
	 * 其他地方的决策相互作用，后者也会对
	 * 带有参数输入的合并连接进行歧视；请参见
	 * src/backend/optimizer/README 中的评论。
	 *
	 * 随着我们缩短排序键列表，我们应该只考虑严格比（特别是，不同于）
	 * 在先前迭代中找到的任何路径便宜的路径。否则我们将故意使用比给定路径允许的更少
	 * 的合并键（将其余部分视为普通连接条件），这可能不是一个好主意。此外，
	 * 仅依据 compare_path_costs 来消除路径的成本远低于
	 * 构建合并连接路径然后丢弃它。
	 *
	 * 如果 inner_cheapest_total 排序良好以至于在上面
	 * 制作的路径中不需要排序，我们也不应该用它制造重复路径。
	 * 我们使用处理上一个考虑事项的相同逻辑来处理该情况，
	 * 通过正确初始化跟踪最便宜当前值的变量。请注意，如果我们发现
	 * inner_cheapest_total 匹配某个更短的路径键集合，
	 * 我们并不会拒绝它。这种情况对应于使用更少的合并键来避免
	 * 对 inner_cheapest_total 进行排序，而我们在上面已经对其进行了排序，
	 * 所以被考虑的计划是不同的。
	 */
	if (pathkeys_contained_in(fc_innersortkeys,
							  fc_inner_cheapest_total->pathkeys))
	{
		/* inner_cheapest_total 不需要排序 */
		fc_cheapest_startup_inner = fc_inner_cheapest_total;
		fc_cheapest_total_inner = fc_inner_cheapest_total;
	}
	else
	{
		/* 它确实需要排序，至少对于完整的键集合 */
		fc_cheapest_startup_inner = NULL;
		fc_cheapest_total_inner = NULL;
	}
	fc_num_sortkeys = list_length(fc_innersortkeys);
	if (fc_num_sortkeys > 1 && !fc_useallclauses)
		fc_trialsortkeys = list_copy(fc_innersortkeys);	/* 需要可修改副本 */
	else
		fc_trialsortkeys = fc_innersortkeys;	/* 不会真正截断 */

	for (fc_sortkeycnt = fc_num_sortkeys; fc_sortkeycnt > 0; fc_sortkeycnt--)
	{
		Path	   *fc_innerpath;
		List	   *fc_newclauses = NIL;

		/*
		 * 查找足够好排序的内部路径，以满足前
		 * 'sortkeycnt' 个内部排序键。注意：trialsortkeys 列表是
		 * 以破坏性方式修改的，这就是我们制作副本的原因……
		 */
		fc_trialsortkeys = list_truncate(fc_trialsortkeys, fc_sortkeycnt);
		fc_innerpath = get_cheapest_path_for_pathkeys(fc_innerrel->pathlist,
												   fc_trialsortkeys,
												   NULL,
												   TOTAL_COST,
												   fc_is_partial);
		if (fc_innerpath != NULL &&
			(fc_cheapest_total_inner == NULL ||
			 compare_path_costs(fc_innerpath, fc_cheapest_total_inner,
								TOTAL_COST) < 0))
		{
			/* 找到了一个便宜（甚至更便宜）的排序路径 */
			/* 如果我们尚未选择合适的合并子句 */
			if (fc_sortkeycnt < fc_num_sortkeys)
			{
				fc_newclauses =
					trim_mergeclauses_for_inner_pathkeys(fc_root,
														 fc_mergeclauses,
														 fc_trialsortkeys);
				Assert(fc_newclauses != NIL);
			}
			else
				fc_newclauses = fc_mergeclauses;
			fc_try_mergejoin_path(fc_root,
							   fc_joinrel,
							   fc_outerpath,
							   fc_innerpath,
							   fc_merge_pathkeys,
							   fc_newclauses,
							   NIL,
							   NIL,
							   fc_jointype,
							   fc_extra,
							   fc_is_partial);
			fc_cheapest_total_inner = fc_innerpath;
		}
		/* 基于最便宜的启动成本来选择 ... */
		fc_innerpath = get_cheapest_path_for_pathkeys(fc_innerrel->pathlist,
												   fc_trialsortkeys,
												   NULL,
												   STARTUP_COST,
												   fc_is_partial);
		if (fc_innerpath != NULL &&
			(fc_cheapest_startup_inner == NULL ||
			 compare_path_costs(fc_innerpath, fc_cheapest_startup_inner,
								STARTUP_COST) < 0))
		{
			/* 找到了一个便宜（甚至更便宜）的排序路径 */
			if (fc_innerpath != fc_cheapest_total_inner)
			{
				/*
				 * 如果我们已经制作了一个子句列表，则避免重建子句列表；可以节省
				 * 大连接树中的内存...
				 */
				if (fc_newclauses == NIL)
				{
					if (fc_sortkeycnt < fc_num_sortkeys)
					{
						fc_newclauses =
							trim_mergeclauses_for_inner_pathkeys(fc_root,
																 fc_mergeclauses,
																 fc_trialsortkeys);
						Assert(fc_newclauses != NIL);
					}
					else
						fc_newclauses = fc_mergeclauses;
				}
				fc_try_mergejoin_path(fc_root,
								   fc_joinrel,
								   fc_outerpath,
								   fc_innerpath,
								   fc_merge_pathkeys,
								   fc_newclauses,
								   NIL,
								   NIL,
								   fc_jointype,
								   fc_extra,
								   fc_is_partial);
			}
			fc_cheapest_startup_inner = fc_innerpath;
		}

		/*
		 * 如果我们需要所有子句，则不要考虑截断的排序键。
		 */
		if (fc_useallclauses)
			break;
	}
}


/*
 * match_unsorted_outer
 *	  创建用于处理单个连接关系 'joinrel' 的可能连接路径
 *	  通过采用迭代替换或在每个可能的外部路径上进行合并连接（仅考虑
 *	  已经足够有序以进行合并的外部路径）。
 *
 * 我们始终为每个可用的外部路径生成一个嵌套循环路径。
 * 实际上，我们可能会生成多达五个：一个在最便宜的总成本
 * 内部路径上，一个在同一条路径上进行物化，一个在
 * 最便宜的启动成本内部路径上（如果不同），一个在
 * 最便宜的总成本内部索引扫描路径上（如果有），以及一个在
 * 最便宜的启动内部索引扫描路径上（如果不同）。
 *
 * 如果合并连接子句可用，我们还会考虑合并连接。请参见
 * generate_mergejoin_paths 中的详细注释。
 *
 * 'joinrel' 是连接关系
 * 'outerrel' 是外连接关系
 * 'innerrel' 是内连接关系
 * 'jointype' 是要执行的连接类型
 * 'extra' 包含额外的输入值
 */
static void fc_match_unsorted_outer(PlannerInfo *fc_root,
					 RelOptInfo *fc_joinrel,
					 RelOptInfo *fc_outerrel,
					 RelOptInfo *fc_innerrel,
					 JoinType fc_jointype,
					 JoinPathExtraData *fc_extra)
{
	JoinType	fc_save_jointype = fc_jointype;
	bool		fc_nestjoinOK;
	bool		fc_useallclauses;
	Path	   *fc_inner_cheapest_total = fc_innerrel->cheapest_total_path;
	Path	   *fc_matpath = NULL;
	ListCell   *fc_lc1;

	/*
	 * 嵌套循环仅支持内部、左、半和反连接。 另外，如果我们
	 * 在执行右连接或全连接时，必须使用 *所有* 合并子句
	 * 作为连接子句，否则我们将没有有效的计划。 （尽管这
	 * 两个标志目前是反向的，但为了清晰和可能的未来变化，将它们分开。）
	 */
	switch (fc_jointype)
	{
		case JOIN_INNER:
		case JOIN_LEFT:
		case JOIN_SEMI:
		case JOIN_ANTI:
			fc_nestjoinOK = true;
			fc_useallclauses = false;
			break;
		case JOIN_RIGHT:
		case JOIN_FULL:
			fc_nestjoinOK = false;
			fc_useallclauses = true;
			break;
		case JOIN_UNIQUE_OUTER:
		case JOIN_UNIQUE_INNER:
			fc_jointype = JOIN_INNER;
			fc_nestjoinOK = true;
			fc_useallclauses = false;
			break;
		default:
			elog(ERROR, "unrecognized join type: %d",
				 (int) fc_jointype);
			fc_nestjoinOK = false; /* 保持编译器安静 */
			fc_useallclauses = false;
			break;
	}

	/*
	 * 如果 inner_cheapest_total 由外部关系参数化，则忽略它；
	 * 我们将在下面作为 cheapest_parameterized_paths 的成员考虑它，
	 * 但本例程中考虑的其他可能性不可用。
	 */
	if (PATH_PARAM_BY_REL(fc_inner_cheapest_total, fc_outerrel))
		fc_inner_cheapest_total = NULL;

	/*
	 * 如果我们需要对内部路径进行唯一化，我们将只考虑
	 * 最便宜的总内部。
	 */
	if (fc_save_jointype == JOIN_UNIQUE_INNER)
	{
		/* 没有办法使用由外部关系参数化的内部路径 */
		if (fc_inner_cheapest_total == NULL)
			return;
		fc_inner_cheapest_total = (Path *)
			create_unique_path(fc_root, fc_innerrel, fc_inner_cheapest_total, fc_extra->sjinfo);
		Assert(fc_inner_cheapest_total);
	}
	else if (fc_nestjoinOK)
	{
		/*
		 * 考虑物化最便宜的内部路径，除非
		 * enable_material 被关闭或所考虑的路径无论如何都会物化其
		 * 输出。
		 */
		if (enable_material && fc_inner_cheapest_total != NULL &&
			!ExecMaterializesOutput(fc_inner_cheapest_total->pathtype))
			fc_matpath = (Path *)
				create_material_path(fc_innerrel, fc_inner_cheapest_total);
	}

	foreach(fc_lc1, fc_outerrel->pathlist)
	{
		Path	   *fc_outerpath = (Path *) lfirst(fc_lc1);
		List	   *fc_merge_pathkeys;

		/*
		 * 我们不能使用由内部关系参数化的外部路径。
		 */
		if (PATH_PARAM_BY_REL(fc_outerpath, fc_innerrel))
			continue;

		/*
		 * 如果我们需要对外部路径进行唯一化，考虑最便宜的外部
		 * 概念是没有意义的。 （XXX 我们不考虑参数化
		 * 外部，也不考虑内部，对于唯一化情况。我们应该吗？）
		 */
		if (fc_save_jointype == JOIN_UNIQUE_OUTER)
		{
			if (fc_outerpath != fc_outerrel->cheapest_total_path)
				continue;
			fc_outerpath = (Path *) create_unique_path(fc_root, fc_outerrel,
													fc_outerpath, fc_extra->sjinfo);
			Assert(fc_outerpath);
		}

		/*
		 * 结果将具有此排序顺序（即使它是作为
		 * 嵌套循环实现的，即使某些合并子句是通过
		 * qpquals 实现的而不是作为真正的合并子句）：
		 */
		fc_merge_pathkeys = build_join_pathkeys(fc_root, fc_joinrel, fc_jointype,
											 fc_outerpath->pathkeys);

		if (fc_save_jointype == JOIN_UNIQUE_INNER)
		{
			/*
			 * 考虑嵌套循环连接，但仅与唯一化的最便宜
			 * 内部路径
			 */
			fc_try_nestloop_path(fc_root,
							  fc_joinrel,
							  fc_outerpath,
							  fc_inner_cheapest_total,
							  fc_merge_pathkeys,
							  fc_jointype,
							  fc_extra);
		}
		else if (fc_nestjoinOK)
		{
			/*
			 * 考虑使用这个外部路径的嵌套循环连接，以及内关系的各种可用路径。我们考虑每个可用参数化内关系的最便宜总路径，包括未参数化的情况。
			 */
			ListCell   *fc_lc2;

			foreach(fc_lc2, fc_innerrel->cheapest_parameterized_paths)
			{
				Path	   *fc_innerpath = (Path *) lfirst(fc_lc2);
				Path	   *fc_mpath;

				fc_try_nestloop_path(fc_root,
								  fc_joinrel,
								  fc_outerpath,
								  fc_innerpath,
								  fc_merge_pathkeys,
								  fc_jointype,
								  fc_extra);

				/*
				 * 尝试生成一个记忆化路径，看看这是否会使嵌套循环更便宜。
				 */
				fc_mpath = get_memoize_path(fc_root, fc_innerrel, fc_outerrel,
										 fc_innerpath, fc_outerpath, fc_jointype,
										 fc_extra);
				if (fc_mpath != NULL)
					fc_try_nestloop_path(fc_root,
									  fc_joinrel,
									  fc_outerpath,
									  fc_mpath,
									  fc_merge_pathkeys,
									  fc_jointype,
									  fc_extra);
			}

			/* 还要考虑最便宜的内路径的物化形式 */
			if (fc_matpath != NULL)
				fc_try_nestloop_path(fc_root,
								  fc_joinrel,
								  fc_outerpath,
								  fc_matpath,
								  fc_merge_pathkeys,
								  fc_jointype,
								  fc_extra);
		}

		/* 如果外部路径需要唯一化，则无法做其他事情 */
		if (fc_save_jointype == JOIN_UNIQUE_OUTER)
			continue;

		/* 如果内关系由外部参数化，则无法做其他事情 */
		if (fc_inner_cheapest_total == NULL)
			continue;

		/* 生成合并连接路径 */
		fc_generate_mergejoin_paths(fc_root, fc_joinrel, fc_innerrel, fc_outerpath,
								 fc_save_jointype, fc_extra, fc_useallclauses,
								 fc_inner_cheapest_total, fc_merge_pathkeys,
								 false);
	}

	/*
	 * 如果外部关系有任何部分路径且连接关系是并行安全的，则考虑部分嵌套循环和合并连接计划。然而，我们无法处理JOIN_UNIQUE_OUTER，因为外部路径将是部分的，因此我们不能正确保证唯一性。我们也无法处理需要侧向关系的连接，因为部分路径不得参数化。同样，我们无法处理JOIN_FULL和JOIN_RIGHT，因为它们可能会产生虚假的空扩展行。
	 */
	if (fc_joinrel->consider_parallel &&
		fc_save_jointype != JOIN_UNIQUE_OUTER &&
		fc_save_jointype != JOIN_FULL &&
		fc_save_jointype != JOIN_RIGHT &&
		fc_outerrel->partial_pathlist != NIL &&
		bms_is_empty(fc_joinrel->lateral_relids))
	{
		if (fc_nestjoinOK)
			fc_consider_parallel_nestloop(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
									   fc_save_jointype, fc_extra);

		/*
		 * 如果inner_cheapest_total为NULL或非并行安全，则找到最便宜的总并行安全路径。如果进行JOIN_UNIQUE_INNER，则我们不能使用任何替代内路径。
		 */
		if (fc_inner_cheapest_total == NULL ||
			!fc_inner_cheapest_total->parallel_safe)
		{
			if (fc_save_jointype == JOIN_UNIQUE_INNER)
				return;

			fc_inner_cheapest_total = get_cheapest_parallel_safe_total_inner(fc_innerrel->pathlist);
		}

		if (fc_inner_cheapest_total)
			fc_consider_parallel_mergejoin(fc_root, fc_joinrel, fc_outerrel, fc_innerrel,
										fc_save_jointype, fc_extra,
										fc_inner_cheapest_total);
	}
}

/*
 * consider_parallel_mergejoin
 *	  尝试通过将外关系的部分路径与内关系的完整路径连接，构建连接关系的部分路径。
 *
 * 'joinrel'是连接关系
 * 'outerrel'是外连接关系
 * 'innerrel'是内连接关系
 * 'jointype'是要进行的连接类型
 * 'extra'包含附加输入值
 * 'inner_cheapest_total'是内关系的最便宜总路径
 */
static void fc_consider_parallel_mergejoin(PlannerInfo *fc_root,
							RelOptInfo *fc_joinrel,
							RelOptInfo *fc_outerrel,
							RelOptInfo *fc_innerrel,
							JoinType fc_jointype,
							JoinPathExtraData *fc_extra,
							Path *fc_inner_cheapest_total)
{
	ListCell   *fc_lc1;

	/* 为每个部分外路径生成合并连接路径 */
	foreach(fc_lc1, fc_outerrel->partial_pathlist)
	{
		Path	   *fc_outerpath = (Path *) lfirst(fc_lc1);
		List	   *fc_merge_pathkeys;

		/*
		 * 确定我们创建的任何路径将具有什么有用的排序。
		 */
		fc_merge_pathkeys = build_join_pathkeys(fc_root, fc_joinrel, fc_jointype,
											 fc_outerpath->pathkeys);

		fc_generate_mergejoin_paths(fc_root, fc_joinrel, fc_innerrel, fc_outerpath, fc_jointype,
								 fc_extra, false, fc_inner_cheapest_total,
								 fc_merge_pathkeys, true);
	}
}

/*
 * consider_parallel_nestloop
 *	  尝试通过将外关系的部分路径与内关系的完整路径连接，构建连接关系的部分路径。
 *
 * 'joinrel'是连接关系
 * 'outerrel'是外连接关系
 * 'innerrel'是内连接关系
 * 'jointype'是要进行的连接类型
 * 'extra'包含附加输入值
 */
static void fc_consider_parallel_nestloop(PlannerInfo *fc_root,
						   RelOptInfo *fc_joinrel,
						   RelOptInfo *fc_outerrel,
						   RelOptInfo *fc_innerrel,
						   JoinType fc_jointype,
						   JoinPathExtraData *fc_extra)
{
	JoinType	fc_save_jointype = fc_jointype;
	ListCell   *fc_lc1;

	if (fc_jointype == JOIN_UNIQUE_INNER)
		fc_jointype = JOIN_INNER;

	foreach(fc_lc1, fc_outerrel->partial_pathlist)
	{
		Path	   *fc_outerpath = (Path *) lfirst(fc_lc1);
		List	   *fc_pathkeys;
		ListCell   *fc_lc2;

		/* 确定我们创建的任何路径将具有什么有用的排序。 */
		fc_pathkeys = build_join_pathkeys(fc_root, fc_joinrel, fc_jointype,
									   fc_outerpath->pathkeys);

		/*
		 * 尝试最便宜的参数化路径；只有那些与此外部关系连接时会产生未参数化路径的路径才会通过try_partial_nestloop_path。最便宜的未参数化路径也在此列表中。
		 */
		foreach(fc_lc2, fc_innerrel->cheapest_parameterized_paths)
		{
			Path	   *fc_innerpath = (Path *) lfirst(fc_lc2);
			Path	   *fc_mpath;

			/* 不能连接到不安全的内部路径 */
			if (!fc_innerpath->parallel_safe)
				continue;

			/*
			 * 如果我们正在进行JOIN_UNIQUE_INNER，则只能使用内关系的最便宜总路径，并且必须对其进行唯一化。（我们可能能够放宽此要求，以允许其他安全的、未参数化的内路径，但现在create_unique_path对此没有达成一致。）
			 */
			if (fc_save_jointype == JOIN_UNIQUE_INNER)
			{
				if (fc_innerpath != fc_innerrel->cheapest_total_path)
					continue;
				fc_innerpath = (Path *) create_unique_path(fc_root, fc_innerrel,
														fc_innerpath,
														fc_extra->sjinfo);
				Assert(fc_innerpath);
			}

			fc_try_partial_nestloop_path(fc_root, fc_joinrel, fc_outerpath, fc_innerpath,
									  fc_pathkeys, fc_jointype, fc_extra);

			/*
			 * 尝试生成一个记忆化路径，看看这是否会使嵌套循环更便宜。
			 */
			fc_mpath = get_memoize_path(fc_root, fc_innerrel, fc_outerrel,
									 fc_innerpath, fc_outerpath, fc_jointype,
									 fc_extra);
			if (fc_mpath != NULL)
				fc_try_partial_nestloop_path(fc_root, fc_joinrel, fc_outerpath, fc_mpath,
										  fc_pathkeys, fc_jointype, fc_extra);
		}
	}
}

/*
 * hash_inner_and_outer
 *	  通过明确哈希每个可用哈希子句的外部和内部键，创建哈希连接路径。
 *
 * 'joinrel'是连接关系
 * 'outerrel'是外连接关系
 * 'innerrel'是内连接关系
 * 'jointype'是要进行的连接类型
 * 'extra'包含附加输入值
 */
static void fc_hash_inner_and_outer(PlannerInfo *fc_root,
					 RelOptInfo *fc_joinrel,
					 RelOptInfo *fc_outerrel,
					 RelOptInfo *fc_innerrel,
					 JoinType fc_jointype,
					 JoinPathExtraData *fc_extra)
{
	JoinType	fc_save_jointype = fc_jointype;
	bool		fc_isouterjoin = IS_OUTER_JOIN(fc_jointype);
	List	   *fc_hashclauses;
	ListCell   *fc_l;

	/*
	 * 我们只需要为任何给定的外部和内部关系构建一个hashclauses列表；所有可哈希的子句都将用作键。
	 *
	 * 扫描连接的restrictinfo列表，找到可与这对子关系一起使用的哈希连接子句。
	 */
	fc_hashclauses = NIL;
	foreach(fc_l, fc_extra->restrictlist)
	{
		RestrictInfo *fc_restrictinfo = (RestrictInfo *) lfirst(fc_l);

		/*
		 * 如果处理外连接，仅使用其自身的连接子句进行哈希处理。对于内部连接，我们不必如此挑剔。
		 */
		if (fc_isouterjoin && RINFO_IS_PUSHED_DOWN(fc_restrictinfo, fc_joinrel->relids))
			continue;

		if (!fc_restrictinfo->can_join ||
			fc_restrictinfo->hashjoinoperator == InvalidOid)
			continue;			/* 不是可哈希连接的 */

		/*
		 * 检查子句是否具有“outer op inner”或“inner op outer”的形式。
		 */
		if (!fc_clause_sides_match_join(fc_restrictinfo, fc_outerrel, fc_innerrel))
			continue;			/* 对这些输入关系不适用 */

		fc_hashclauses = lappend(fc_hashclauses, fc_restrictinfo);
	}

	/* 如果我们找到任何可用的哈希子句，创建路径 */
	if (fc_hashclauses)
	{
		/*
		 * 我们同时考虑最便宜的总成本和最便宜的启动成本外路径。然而，并不需要考虑任何但最便宜的总成本内路径。
		 */
		Path	   *fc_cheapest_startup_outer = fc_outerrel->cheapest_startup_path;
		Path	   *fc_cheapest_total_outer = fc_outerrel->cheapest_total_path;
		Path	   *fc_cheapest_total_inner = fc_innerrel->cheapest_total_path;

		/*
		 * 如果任何最便宜的总路径被其他关系参数化，则我们无法使用哈希连接。（没有必要寻找备用输入路径，因为这些应该已经是最少参数化的可用路径。）
		 */
		if (PATH_PARAM_BY_REL(fc_cheapest_total_outer, fc_innerrel) ||
			PATH_PARAM_BY_REL(fc_cheapest_total_inner, fc_outerrel))
			return;

		/* 如果需要进行唯一化；我们忽略参数化的可能性 */
		if (fc_jointype == JOIN_UNIQUE_OUTER)
		{
			fc_cheapest_total_outer = (Path *)
				create_unique_path(fc_root, fc_outerrel,
								   fc_cheapest_total_outer, fc_extra->sjinfo);
			Assert(fc_cheapest_total_outer);
			fc_jointype = JOIN_INNER;
			fc_try_hashjoin_path(fc_root,
							  fc_joinrel,
							  fc_cheapest_total_outer,
							  fc_cheapest_total_inner,
							  fc_hashclauses,
							  fc_jointype,
							  fc_extra);
			/* 这里没有便宜的启动的可能性 */
		}
		else if (fc_jointype == JOIN_UNIQUE_INNER)
		{
			fc_cheapest_total_inner = (Path *)
				create_unique_path(fc_root, fc_innerrel,
								   fc_cheapest_total_inner, fc_extra->sjinfo);
			Assert(fc_cheapest_total_inner);
			fc_jointype = JOIN_INNER;
			fc_try_hashjoin_path(fc_root,
							  fc_joinrel,
							  fc_cheapest_total_outer,
							  fc_cheapest_total_inner,
							  fc_hashclauses,
							  fc_jointype,
							  fc_extra);
			if (fc_cheapest_startup_outer != NULL &&
				fc_cheapest_startup_outer != fc_cheapest_total_outer)
				fc_try_hashjoin_path(fc_root,
								  fc_joinrel,
								  fc_cheapest_startup_outer,
								  fc_cheapest_total_inner,
								  fc_hashclauses,
								  fc_jointype,
								  fc_extra);
		}
		else
		{
			
/*
			 * 对于其他连接类型，我们考虑最便宜的启动外部
			 * 以及最便宜的总内部，然后考虑
			 * 包括参数化路径在内的最便宜总路径的配对。
			 * 根据可能便宜的启动成本生成参数化路径是没有意义的，
			 * 所以这就足够了。
			 */
			ListCell   *fc_lc1;
			ListCell   *fc_lc2;

			if (fc_cheapest_startup_outer != NULL)
				fc_try_hashjoin_path(fc_root,
								  fc_joinrel,
								  fc_cheapest_startup_outer,
								  fc_cheapest_total_inner,
								  fc_hashclauses,
								  fc_jointype,
								  fc_extra);

			foreach(fc_lc1, fc_outerrel->cheapest_parameterized_paths)
			{
				Path	   *fc_outerpath = (Path *) lfirst(fc_lc1);

				/*
				 * 我们不能使用一个由
				 * 内部关系参数化的外部路径。
				 */
				if (PATH_PARAM_BY_REL(fc_outerpath, fc_innerrel))
					continue;

				foreach(fc_lc2, fc_innerrel->cheapest_parameterized_paths)
				{
					Path	   *fc_innerpath = (Path *) lfirst(fc_lc2);

					/*
					 * 我们也不能使用由外部关系参数化的内部路径。
					 */
					if (PATH_PARAM_BY_REL(fc_innerpath, fc_outerrel))
						continue;

					if (fc_outerpath == fc_cheapest_startup_outer &&
						fc_innerpath == fc_cheapest_total_inner)
						continue;	/* 已经尝试过了 */

					fc_try_hashjoin_path(fc_root,
									  fc_joinrel,
									  fc_outerpath,
									  fc_innerpath,
									  fc_hashclauses,
									  fc_jointype,
									  fc_extra);
				}
			}
		}

		/*
		 * 如果 joinrel 是并行安全的，我们可能考虑部分哈希连接。
		 * 然而，我们不能处理 JOIN_UNIQUE_OUTER，
		 * 因为外部路径将是部分的，因此我们无法妥善保证唯一性。
		 * 同样，我们不能处理 JOIN_FULL 和 JOIN_RIGHT，
		 * 因为它们可能会产生虚假空值扩展行。
		 * 此外，结果路径必须不可参数化。
		 * 我们能够支持 Parallel Hash 的 JOIN_FULL 和 JOIN_RIGHT，
		 * 因为在那种情况下我们回到单个哈希表，
		 * 每个批次有一组匹配位，但这将要求找出一种无死锁的方法来等待探测完成。
		 */
		if (fc_joinrel->consider_parallel &&
			fc_save_jointype != JOIN_UNIQUE_OUTER &&
			fc_save_jointype != JOIN_FULL &&
			fc_save_jointype != JOIN_RIGHT &&
			fc_outerrel->partial_pathlist != NIL &&
			bms_is_empty(fc_joinrel->lateral_relids))
		{
			Path	   *fc_cheapest_partial_outer;
			Path	   *fc_cheapest_partial_inner = NULL;
			Path	   *fc_cheapest_safe_inner = NULL;

			fc_cheapest_partial_outer =
				(Path *) linitial(fc_outerrel->partial_pathlist);

			/*
			 * 我们能否使用部分内部计划，使得我们可以并行构建共享哈希表？
			 * 我们无法处理 JOIN_UNIQUE_INNER，因为我们无法保证唯一性。
			 */
			if (fc_innerrel->partial_pathlist != NIL &&
				fc_save_jointype != JOIN_UNIQUE_INNER &&
				enable_parallel_hash)
			{
				fc_cheapest_partial_inner =
					(Path *) linitial(fc_innerrel->partial_pathlist);
				fc_try_partial_hashjoin_path(fc_root, fc_joinrel,
										  fc_cheapest_partial_outer,
										  fc_cheapest_partial_inner,
										  fc_hashclauses, fc_jointype, fc_extra,
										  true /* parallel_hash */ );
			}

			/*
			 * 通常，考虑到 joinrel 是并行安全的，最便宜的
			 * 总内部路径也将是并行安全的，但如果不是，我们将
			 * 必须搜索最便宜的安全、不可参数化的内部路径。
			 * 如果进行 JOIN_UNIQUE_INNER，我们无法使用任何替代的
			 * 内部路径。
			 */
			if (fc_cheapest_total_inner->parallel_safe)
				fc_cheapest_safe_inner = fc_cheapest_total_inner;
			else if (fc_save_jointype != JOIN_UNIQUE_INNER)
				fc_cheapest_safe_inner =
					get_cheapest_parallel_safe_total_inner(fc_innerrel->pathlist);

			if (fc_cheapest_safe_inner != NULL)
				fc_try_partial_hashjoin_path(fc_root, fc_joinrel,
										  fc_cheapest_partial_outer,
										  fc_cheapest_safe_inner,
										  fc_hashclauses, fc_jointype, fc_extra,
										  false /* parallel_hash */ );
		}
	}
}

/*
 * select_mergejoin_clauses
 *	  选择适用于特定连接的合并连接子句。
 *	  返回这些子句的 RestrictInfo 节点列表。
 *
 * *mergejoin_allowed 通常设置为 true，但如果这是右连接/完全连接并且存在不可合并连接子句，则设置为 false。
 * 执行器的合并连接机制无法处理这种情况，因此我们必须避免生成合并连接计划。
 * （注意，这个标志并不会考虑是否实际存在任何可合并的子句。
 * 这是正确的，因为在某些情况下我们需要构建无子句的合并连接。
 * 仅仅返回 NIL 因此不足以区分安全和不安全的情况。）
 *
 * 我们还标记每个选定的 RestrictInfo，以显示当前被视为外部的哪一侧。
 * 这些是临时标记，仅在当前 add_paths_to_joinrel() 调用期间有效！
 *
 * 我们检查与连接相关的每个 restrictinfo 子句，以查看
 * 它是否可合并并涉及当前感兴趣的两个子关系的变量。
 */
static List * fc_select_mergejoin_clauses(PlannerInfo *fc_root,
						 RelOptInfo *fc_joinrel,
						 RelOptInfo *fc_outerrel,
						 RelOptInfo *fc_innerrel,
						 List *fc_restrictlist,
						 JoinType fc_jointype,
						 bool *fc_mergejoin_allowed)
{
	List	   *fc_result_list = NIL;
	bool		fc_isouterjoin = IS_OUTER_JOIN(fc_jointype);
	bool		fc_have_nonmergeable_joinclause = false;
	ListCell   *fc_l;

	foreach(fc_l, fc_restrictlist)
	{
		RestrictInfo *fc_restrictinfo = (RestrictInfo *) lfirst(fc_l);

		/*
		 * 如果处理外部连接，仅使用其自己的连接子句进行合并。
		 * 对于内部连接，我们也可以使用向下推送的子句。（注意：
		 * 我们在这里不设置 have_nonmergeable_joinclause，因为向下推送的
		 * 子句将变为其他条件，而不是连接条件。）
		 */
		if (fc_isouterjoin && RINFO_IS_PUSHED_DOWN(fc_restrictinfo, fc_joinrel->relids))
			continue;

		/* 检查子句是否为可合并操作符子句 */
		if (!fc_restrictinfo->can_join ||
			fc_restrictinfo->mergeopfamilies == NIL)
		{
			/*
			 * 当进行右连接/完全合并连接时，执行器可以处理作为常量的额外连接条件，
			 * 但不能处理其他任何情况。（支持常量的原因是为了让我们能够做 FULL JOIN ON FALSE。）
			 */
			if (!fc_restrictinfo->clause || !IsA(fc_restrictinfo->clause, Const))
				fc_have_nonmergeable_joinclause = true;
			continue;			/* 不是可合并连接的 */
		}

		/*
		 * 检查子句是否具有“outer op inner”或“inner op outer”的形式。
		 */
		if (!fc_clause_sides_match_join(fc_restrictinfo, fc_outerrel, fc_innerrel))
		{
			fc_have_nonmergeable_joinclause = true;
			continue;			/* 对这些输入关系不适用 */
		}

		/*
		 * 坚持要求每一侧都有一个非冗余的 eclass。
		 * 这个限制是必要的，因为规划器的不同部分期望
		 * 合并中的每个子句都能与规范路径键列表中的某些路径键相关联，
		 * 但冗余的 eclasses 不能出现在规范排序中。
		 * （XXX 可能值得放宽这个限制，但没有足够的时间处理它以适应 8.3。）
		 *
		 * 注意：如果这个条件对本应可合并的 FULL JOIN 子句失败，会非常糟糕，
		 * 因为那将导致不希望看到的规划器失败。
		 * 不过我相信这不可能发生；涉及完全连接的变量只能出现在
		 * below_outer_join eclasses 中，它们不被视为冗余。
		 *
		 * 这个情况 *可能* 会发生在左/右连接子句中：外侧的变量可能等于常量。
		 * 因为我们将传播该常量通过连接子句，
		 * 丧失进行合并连接的能力并不是一个大问题，
		 * 所以不明确地改善这一点是否重要。
		 */
		update_mergeclause_eclasses(fc_root, fc_restrictinfo);

		if (EC_MUST_BE_REDUNDANT(fc_restrictinfo->left_ec) ||
			EC_MUST_BE_REDUNDANT(fc_restrictinfo->right_ec))
		{
			fc_have_nonmergeable_joinclause = true;
			continue;			/* 不能处理冗余 eclasses */
		}

		fc_result_list = lappend(fc_result_list, fc_restrictinfo);
	}

	/*
	 * 报告是否允许合并连接（参见函数顶部的注释）。
	 */
	switch (fc_jointype)
	{
		case JOIN_RIGHT:
		case JOIN_FULL:
			*fc_mergejoin_allowed = !fc_have_nonmergeable_joinclause;
			break;
		default:
			*fc_mergejoin_allowed = true;
			break;
	}

	return fc_result_list;
}
